package net.wanderinghorse.jsonmessage;

/**
    A base type for RequestPoster implementations which takes care of some of
    the more intricate/error-prone parts of the various Request-posting callbacks.

    Misfeatures and Limitations vis-a-vis the JS-specified interface:

    - In asynchronous mode, if the app exits before the post is finished,
    the callbacks might not be called.

    - The timeout post option is not honored by this code. To implement that
    we would need a way of forcibly stopping threads from somewhere outside
    that thread, and the Java API doesn't appear to support this except with
    deprecated methods.

    - In async mode the beforePost() handler is called in the async thread,
    not the calling thread. This is not a bug in and of itself but differs from
    the JS API, which calls beforePost() before the back-end is called, because
    the async handling is much different in JS space (and depends on support in
    each back-end, whereas in Java this class does the async part).
*/
public abstract class AbstractRequestPoster implements RequestPoster {
    private static final ThreadGroup THREAD_GROUP =
            new ThreadGroup(AbstractRequestPoster.class.getName()+" async posts");
    // i'd like to have one ThreadGroup per instance, but there's no way for me to then remove the
    // per-instance group from THREAD_GROUP except by making it a daemon group, which i can't do
    // because we can have post()s coming in at arbitrary times, and the group would normally be
    // empty (meaning it would automatically be destroyed because it's a daemon).
    //private ThreadGroup threadGroup = new ThreadGroup(THREAD_GROUP,AbstractRequestPoster.class.getName()+" Instance");
    /**
     * Default-initializes this object.
     */
    public AbstractRequestPoster() {
    }

    /**
     * Intended to be called by post() after a response comes back. The contents
     * of the response is irrelevant: if a response returns (regardless of its
     * errorCode), it should be passed here.
     *
     * This function essentially does the following:
     *
     * - If data is null then onPostError() is called to trigger the afterPost()
     * and onError() callbacks, and then this function returns.
     *
     * - Calls the afterPost() callback, if not null.
     *
     * - If data is a string, it is converted to a Response object
     * (which may throw if the input is not JSON for a Response).
     * ResponseLoader is used to try to load a response based on the
     * req.getType(), but if no handler is found a BasicResponse
     * instance is used.
     *
     * - If data is-a response object, it is used as-is.
     *
     * - If data is neither a String nor Response then the onError()
     * callback is triggered.
     *
     * - If all goes well, the onResponse() callback is triggered.
     *
     * Exceptions are silently ignored for the afterPost() and onError() callbacks.
     * Exceptions from the onResponse() callback will cause the onError() callback to be
     * triggered.
     *
     * @param req The request to which we are responding.
     * @param data Either a JSON string encoding a Response object or a Response object.
     * Results are technically undefined if the response was not generated in response to
     * the given request object (but it might work).
     * @param opt The connection options used by the request. It may be modified by
     * this call, and should not be used by the client after calling this.
     * @throws IllegalArgumentException If req or opt are null.
     */
    protected final void onResponse( Request req, Object data, PostOptions opt ) {
        if( (null == req) || (null == opt) ) {
           throw new IllegalArgumentException(getClass().getName()+".onResponse(): neither "+
                    "none of the arguments may be null!");
        }
        if( null == data ) {
           opt.setErrorMessage( "Fetched response data is null!");
           this.onPostError( req, opt );
           return;
        }
        if( ! doAfterPost(req,opt) ) return;
        RequestHandler onResponse = opt.getOnResponse();
        if( null == onResponse ) return;
        Response resp = (data instanceof Response) ? (Response)data : null;
        String json = (null == resp) ? data.toString() : null;
        try {
            if( null != json ) {
                resp = ResponseLoader.instance().loadResponse( req.getType(), false );
                if( null == resp ) resp = new Response();
                resp.fromJSON( json );
                if( null == resp.getRequestId() ) {
                    // this should only happen for dispatching-side error responses
                    resp.setRequestId( req.getId() );
                }
            }
            onResponse.processResponse(req, resp);
        }
        catch( Exception ex ) {
            // Reminder to self: don't call this.onPostError() b/c that will trigger afterPost() again.
            opt.setErrorMessage( "Exception ("+ex.getClass().getName()+") during onResponse() callback: "
                                  +ex.toString() );
            callOnError(req, opt);
        }
    }
    /**
     * Intended to be called if post() fails. This routine does the following:
     *
     * - Invokes the afterPost() callback, if not null.
     * - Invokes the onError() callback, if not null.
     *
     * Exceptions thrown by either callback are silently ignored.
     *
     * In both cases, any exceptions they throw are silently ignored.
     *
     * The caller "really should" call opt.setErrorMessage() BEFORE calling this,
     * to give the onError() callback some information regarding the specifics
     * of the error.
     *
     * @param req The request for which the error was triggered.
     * @param opt The options of the request.
     */
    protected final void onPostError( Request req, PostOptions opt ) {
        if( ! doAfterPost(req,opt) ) return;
        this.callOnError(req,opt);
    }

    /**
     * Calls the onError() callback, if it is not null. Does not trigger afterPost().
     * @param req The request object.
     * @param opt Object which might contain an onError() callback.
     */
    protected final void callOnError( Request req, PostOptions opt ) {
        CallbackRequestOpt cb = opt.getOnError();
        if( null != cb ) {
            try {
                cb.invoke( req, opt );
            }
            catch( Exception e ) {
                /* ignore */
            }
        }
    }

    /**
     * Performs the beforePost() or afterPost() callback, if not null.
     *
     * If it returns false the post() implementation must not do
     * anything further and must return. If it returns false the
     * caller must not call the afterPost() or onError() handlers.
     *
     * @param isBefore If true then the beforePost() callback is used, else the afterPost() callback.
     * @param r Same as for post().
     * @param opt Same as for post()
     * @return a boolean indicating whether the post should proceed.
     */
    private boolean doBeforeAfterPost( boolean isBefore, Request r, PostOptions opt) {
        CallbackRequestOpt cb = isBefore ? opt.getBeforePost() : opt.getAfterPost();
        if( null == cb ) return true;
        try {
            cb.invoke( r, opt );
            return true;
        }
        catch(Exception e) {
            /* Conventionally, neither the beforePost() nor afterPost() trigger
               further processing on error. We don't throw here because the JS
               API doesn't (or at least isn't supposed to). And because it can
               cause an exception to leak out and kill running threads. That thinking
               needs to be reconsidered, though. 100% behavioural compatibility with the JS API
               is not necessarily a goal here.

               TODO: consider passing on the exceptions here.
             */
//            opt.setErrorMessage( "Exception ("+e.getClass().getName()+") during "
//                    +(isBefore?"before":"after")+"Post(): "+e.toString() );
//            this.callOnError( r, opt );
            return false;

        }
    }
    private boolean doBeforePost( Request r, PostOptions opt) {
        return doBeforeAfterPost( true, r, opt );
    }
    private boolean doAfterPost( Request r, PostOptions opt) {
        return doBeforeAfterPost( false, r, opt );
    }

    /**
     * Must be implemented to perform a post operation on behalf of the given request.
     * If the connection returns a response, onResponse() should be called, otherwise
     * onPostError() should be called. On error, implementations should call opt.setErrorMessage()
     * before calling onPostError(), so that the onError() callback can get information regarding
     * the error.
     *
     * The caller must pass this function:
     *
     * - The request object to post.
     *
     * - A normalized/fully-populated copy of the PostOptions to use for the connection,
     * as described in the library manual and JS API.
     *
     * If opt.isAsync() then this runs the request in a new thread, but that
     * thread still refers back to the r and opt objects, so they must not
     * be changed while the post() is running. Doing so will lead to undefined
     * results. When running asynchronous threads, it is important that each
     * call to this function get its own (deep) copy of the opt object. It is
     * legal, but rarely useful, for multiple posts to use the same Request
     * object (which is not changed by the post operation).
     *
     * @see RequestPoster
     * @param r The request to post.
     * @param opt The connection options to use for the request.
     * @throws Exception Implementations may throw arbitrary exceptions, but are encouraged to
     * report errors via the framework-specified callbacks.
     */
    protected abstract void doPost( Request r, PostOptions opt) throws Exception;


    /**
     * @param key Response type lookup key.
     * @return Response instance for the key, or null if none found.
     */
// not used?
//    protected Response loadResponse( String key )
//    {
//        return ResponseLoader.instance().loadResponse( key, false );
//    }
    /**
     * @param key ResponseHandler type lookup key.
     * @return ResponseHandler instance for the key, or null if none found. 
     */
    protected RequestHandler loadRequestHandler( String key )
    {
        return RequestHandlerLoader.instance().loadRequestHandler( key );
    }

    /**
     * Code duplication saver. Runs the beforePost() callback (if any), and
     * calls doPost() if beforePost() succeeds (or is null). On error it calls
     * onPostError().
     * @param r The input request.
     * @param opt Post options, which may be modified by this call.
     */
    private void runPost( final Request r, final PostOptions opt) {
        if( ! this.doBeforePost( r, opt) ) return;
        try {
            this.doPost( r, opt );
        }
        catch( Exception ex ) {
            opt.setErrorMessage( "Exception during post(): "+ex.toString() );
            this.onPostError( r, opt );
        }
    }
    /**
     * Performs the initial work related to posting a request, then calls
     * doPost() to perform the connection-specific work.
     *
     * If opt.isAsync() then this runs the request in a new thread, but that
     * thread still refers back to the r and opt objects, so they must not
     * be changed while the post() is running. Doing so will lead to undefined
     * results. When running asynchronous threads, it is important that each
     * call to this function get its own (deep) copy of the opt object. It is
     * legal, but rarely useful, for multiple posts to use the same Request
     * object (which is not changed by the post operation).
     *
     * Any exceptions will be passed to the opt.getOnError() callback,
     * and are not propagated by this function.
     *
     * opt may be modified by this function.
     *
     * Possible bug: the beforePost() handler is called in the same thread
     * as the post itself. Thus async requests will run beforePost()
     * in their child thread. This behaviour differs from the JS API,
     * where doPost() is run before a background task is started. Which
     * behaviour is "most correct" is a philosophical question.
     *
     * @param r The request to post.
     * @param opt The connection options to use for the request.
    */
    public final void post( final Request r, final PostOptions opt) {
        final Boolean async = opt.isAsync();
        if( (null == async) || !async ) {
            this.runPost(r, opt);
            return;
        }
        synchronized (THREAD_GROUP) {
            final AbstractRequestPoster self = this;
            (new Thread(THREAD_GROUP,
                "aysnc post() Request[id="+r.getId()+"]" ) {
                public void run() {
                    // FIXME: (WTF?) ignore exceptions in doBeforePost() here, and note that difference
                    // in the interface docs.
                    self.runPost(r, opt);
                }
            }).start();
        }
    }

    /**
     * Each time an asynchronous post() is made, a thread is started to run
     * the request. The number of active async posts can be fetched by
     * calling this function.
     * 
     * @return The number of active asynchronous post() request threads.
     */
    public static int getActiveAsyncPostCount() {
        /** Without the synchronized block and the extra level of
         * int redirection, i'm not getting the results i expect (i'm always
         * getting a value of at least 1).
         */
        int x;
        synchronized (THREAD_GROUP) {

            x = THREAD_GROUP.activeCount();
        }
        return x;
    }
}
